//--------------------------------------------------------------------------------
// This file is part of the Windows Workflow Foundation Sample Code
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// This source code is intended only as a supplement to Microsoft
// Development Tools and/or on-line documentation.  See these other
// materials for detailed information regarding Microsoft code samples.
// 
// THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//--------------------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Diagnostics;
using System.Web;
using System.Collections.ObjectModel;
using System.Threading;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using System.Configuration;
using System.IO;
using System.Xml;
using System.ComponentModel.Design;
using System.Workflow.ComponentModel.Compiler;
using System.Reflection;

namespace Microsoft.Samples.Workflow.UI.Asp
{
    /// <summary>
    /// Cookie based rotuing module implementation
    /// </summary>
    public class WFWebHostingModule : IHttpModule
    {
        public WFWebHostingModule()
        {
        }

        void IHttpModule.Init(HttpApplication application)
        {
            application.PreRequestHandlerExecute += this.BeginRequest;
            application.PostRequestHandlerExecute += this.EndRequest;

            HttpContext httpContext = application.Context;
            NavigationManager navigationManager = httpContext.Application[ApplicationVariableLookupKeys.NavigationManager] as NavigationManager;
            if (navigationManager == null)
            {
                navigationManager = new NavigationManager(NavigationManagerConfigSection.configSectionName);
                httpContext.Application[ApplicationVariableLookupKeys.NavigationManager] = navigationManager;
            }
            navigationManager.WorkflowRuntime.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(OnWorkflowCompleted);
            navigationManager.WorkflowRuntime.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(OnWorkflowTerminated);

        }

        void IHttpModule.Dispose()
        {
        }


        void BeginRequest(object source, EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;
            NavigationManager navigationManager = application.Application[ApplicationVariableLookupKeys.NavigationManager] as NavigationManager;
            HttpContext httpContext = application.Context;

            if (IsInterationEnabledPage(httpContext.Request) && httpContext.Session != null
                && string.Compare(httpContext.Request.RequestType, "GET", StringComparison.OrdinalIgnoreCase) == 0
                && httpContext.Session[SessionVariableLookupKeys.Exception] == null)
            {
                InteractionContext interactionContext = null;
                Guid workflowId = Guid.Empty;

                Guid idFromQuery = Guid.Empty;
                string flowIdString = httpContext.Request.QueryString["FlowId"] as string;
                if (!string.IsNullOrEmpty(flowIdString))
                {
                    idFromQuery = new Guid(flowIdString);
                }
                Guid idFromSession = Guid.Empty;
                if (httpContext.Session[SessionVariableLookupKeys.InstanceId] != null)
                {
                    idFromSession = (Guid)httpContext.Session[SessionVariableLookupKeys.InstanceId];
                }
                if (idFromQuery != Guid.Empty)
                {
                    workflowId = idFromQuery;
                }
                else
                {
                    if (idFromSession != Guid.Empty && !IsInterationEnabledPage(httpContext.Request))
                    {
                        workflowId = idFromSession;
                    }
                }

                try
                {
                    if (workflowId == Guid.Empty)
                    {
                        // No need to check for error.  The WorkflowRuntime constructor would have done that.
                        NavigationManagerConfigSection settings = ConfigurationManager.GetSection(NavigationManagerConfigSection.configSectionName) as NavigationManagerConfigSection;
                        if (settings.StartOnDemand)
                        {
                            if (string.Compare(httpContext.Request.QueryString["StartWorkflow"], "true", StringComparison.OrdinalIgnoreCase) != 0)
                            {
                                return;
                            }
                        }

                        WorkflowElement workflowElement = settings.Workflow;
                        if (workflowElement == null)
                        {
                            throw new InvalidOperationException(SR.GetString(SR.Error_MissingWorkflowElement, "Workflow", NavigationManagerConfigSection.configSectionName));
                        }

                        Dictionary<string, object> parameters = null;
                        if (settings.StartOnDemand)
                        {
                            parameters = new Dictionary<string, object>();
                            foreach (string parameterName in httpContext.Request.QueryString.Keys)
                            {
                                if (parameterName != "StartWorkflow" && parameterName != "FlowId")
                                {
                                    parameters.Add(parameterName, httpContext.Request.QueryString[parameterName]);
                                }
                            }
                        }

                        if (string.Compare(workflowElement.Mode, "Compiled", StringComparison.OrdinalIgnoreCase) == 0)
                        {
                            Type workflowType = Type.GetType(workflowElement.Value);
                            workflowId = navigationManager.CreateFlow(workflowType, parameters);
                        }
                        else if (string.Compare(workflowElement.Mode, "XOML", StringComparison.OrdinalIgnoreCase) == 0)
                        {
                            if (string.IsNullOrEmpty(workflowElement.RulesFile))
                            {
                                throw new InvalidOperationException(SR.GetString(SR.Error_MissingRulesFileAttribute, "RulesFile", NavigationManagerConfigSection.configSectionName));
                            }

                            if (navigationManager.WorkflowRuntime.GetService(typeof(ITypeProvider)) == null)
                            {
                                TypeProvider typeProvider = new TypeProvider(null);
                                typeProvider.AddAssembly(typeof(Navigator).Assembly);
                                navigationManager.WorkflowRuntime.AddService(typeProvider);
                            }
                            XmlReader xmlReader = XmlTextReader.Create(workflowElement.Value);
                            XmlReader rulesReader = XmlTextReader.Create(workflowElement.RulesFile);
                            workflowId = navigationManager.CreateFlow(xmlReader, rulesReader, parameters);
                        }

                        httpContext.Session[SessionVariableLookupKeys.InstanceId] = workflowId;
                        interactionContext = navigationManager.Start(workflowId);

                        /* Code for direct access to a page. */
                        if (IsDirectaccessible(httpContext.Request))
                        {
                            Bookmark bookmark = new Bookmark(GetBookmarkName(httpContext.Request), Guid.Empty);
                            interactionContext = navigationManager.Navigate(workflowId, bookmark);
                        }

                    }
                    else
                    {
                        if (idFromQuery != Guid.Empty && idFromQuery != idFromSession)
                        {
                            // The previous session for this workflow has being disconnected or the user has requested a different
                            // session. Restart the session by redirecting to the currently blocked page.
                            try
                            {
                                interactionContext = navigationManager.GetCurrentContext(workflowId);
                            }
                            catch (WorkflowNotFoundException)
                            {
                                if (idFromSession != Guid.Empty)
                                {
                                    throw;
                                }
                                else
                                {
                                    //The workflow has ended.  Continue w/o exception.
                                    return;
                                }
                            }

                            httpContext.Session[SessionVariableLookupKeys.InstanceId] = workflowId;
                            string interactionIdString = httpContext.Request.QueryString["InteractionId"] as string;
                            if (!string.IsNullOrEmpty(interactionIdString))
                            {
                                Guid interactionId = new Guid(interactionIdString);
                                interactionContext = NavigateToInteraction(navigationManager, workflowId, interactionId);
                            }
                        }
                        else if (idFromQuery == Guid.Empty && idFromSession != Guid.Empty)
                        {
                            // Start a new workflow
                        }
                    }

                    if (interactionContext != null)
                    {
                        DisplayPage(httpContext, workflowId, interactionContext);
                    }
                }
                catch (Exception exception)
                {
                    DisplayException(httpContext, workflowId, exception);
                }
            }
        }

        void EndRequest(object source, EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;

            NavigationManager navigationManager = application.Application[ApplicationVariableLookupKeys.NavigationManager] as NavigationManager;
            HttpContext httpContext = application.Context;
            if (IsInterationEnabledPage(httpContext.Request) && httpContext.Session != null
                && string.Compare(httpContext.Request.RequestType, "POST", StringComparison.OrdinalIgnoreCase) == 0)
            {
                Guid instanceId = GetWorkflowID(httpContext);
                if (instanceId != Guid.Empty)
                {
                    try
                    {
                        AspNetUserInput input = httpContext.Items[SessionVariableLookupKeys.UserInput] as AspNetUserInput;
                        if (input != null)
                        {
                            if (input.ActionType == ActionType.Forward)
                            {
                                Guid interactionId = Guid.Empty;
                                string interactionIdString = httpContext.Request.QueryString["InteractionId"];
                                if (!string.IsNullOrEmpty(interactionIdString))
                                {
                                    interactionId = new Guid(interactionIdString);
                                }

                                InteractionContext currentContext = navigationManager.GetCurrentContext(instanceId);
                                if (currentContext != null)
                                {
                                    Bookmark moveTo = null;
                                    if (currentContext.Bookmark.InteractionId == interactionId)
                                    {
                                        moveTo = currentContext.Bookmark;
                                    }
                                    else if (currentContext is InteractionGroupContext)
                                    {
                                        // First find out if the request is coming from one of the child pages.
                                        foreach (InteractionContext childContext in ((InteractionGroupContext)currentContext).ActiveChildContexts)
                                        {
                                            if (childContext.Bookmark.InteractionId == interactionId)
                                            {
                                                moveTo = childContext.Bookmark;
                                                break;
                                            }
                                        }
                                    }

                                    // If not from currently active pages, is it from a page in the history list?
                                    if (moveTo == null)
                                    {
                                        currentContext = NavigateToInteraction(navigationManager, instanceId, interactionId);
                                        moveTo = currentContext.Bookmark;
                                    }

                                    // Now move forward by calling Continue with the current bookmark.
                                    // The current bookmark must have aligned with the page, if not an error will be thrown.
                                    currentContext = navigationManager.GoForward(instanceId, moveTo, input);
                                    if (currentContext != null)
                                    {
                                        DisplayPage(httpContext, instanceId, currentContext);
                                    }
                                }
                                else
                                {
                                    throw new InvalidOperationException(SR.GetString(SR.Error_CantGetCurrentContext, instanceId.ToString()));
                                }
                            }
                            else if (input.ActionType == ActionType.Back)
                            {
                                InteractionContext currentContext = navigationManager.GoBack(instanceId);
                                if (currentContext != null)
                                {
                                    DisplayPage(httpContext, instanceId, currentContext);
                                }
                            }
                            else if (input.ActionType == ActionType.Cancel)
                            {
                                navigationManager.Terminate(instanceId);
                            }
                        }
                    }
                    catch (Exception exception)
                    {
                        DisplayException(httpContext, instanceId, exception);
                    }
                }
            }
        }

        private InteractionContext NavigateToInteraction(NavigationManager navigationManager, Guid workflowId, Guid interactionId)
        {
            InteractionContext currentContext = null;
            Bookmark targetBookmark = null;
            foreach (Bookmark bookmark in navigationManager.GetHistory(workflowId))
            {
                if (interactionId == bookmark.InteractionId)
                {
                    targetBookmark = bookmark;
                    break;
                }
            }

            if (targetBookmark != null)
            {
                currentContext = navigationManager.Navigate(workflowId, targetBookmark);
            }
            else
            {
                throw new InvalidOperationException(SR.GetString(SR.Error_InteractionIdNotFound, interactionId.ToString()));
            }

            return currentContext;
        }

        private Guid GetWorkflowID(HttpContext httpContext)
        {
            Guid workflowId = Guid.Empty;
            string instanceIdString = httpContext.Request.QueryString["FlowId"];
            if (!string.IsNullOrEmpty(instanceIdString))
            {
                workflowId = new Guid(instanceIdString);
            }
            if (workflowId == Guid.Empty && httpContext.Session[SessionVariableLookupKeys.InstanceId] != null)
            {
                workflowId = (Guid)httpContext.Session[SessionVariableLookupKeys.InstanceId];
            }
            return workflowId;
        }

        private bool IsInterationEnabledPage(HttpRequest request)
        {
            bool interactionPage = false;
            AspNavigationConfigSection settings = ConfigurationManager.GetSection(AspNavigationConfigSection.configSectionName) as AspNavigationConfigSection;
            foreach (PageMappingElement mapping in settings.PageMappings)
            {
                if (string.Compare(mapping.Location, request.Path, StringComparison.CurrentCultureIgnoreCase) == 0 ||
                    string.Compare(mapping.Location, request.AppRelativeCurrentExecutionFilePath, StringComparison.CurrentCultureIgnoreCase) == 0 ||
                    string.Compare(mapping.Location, request.RawUrl, StringComparison.CurrentCultureIgnoreCase) == 0)
                {
                    interactionPage = true;
                    break;
                }
            }

            return interactionPage;
        }

        /// <summary>
        /// Is a page directly accessible according to the configuration.
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private bool IsDirectaccessible(HttpRequest request)
        {
            bool isDirectaccessible = false;
            AspNavigationConfigSection settings = ConfigurationManager.GetSection(AspNavigationConfigSection.configSectionName) as AspNavigationConfigSection;
            foreach (PageMappingElement mapping in settings.PageMappings)
            {
                if (string.Compare(mapping.Location, request.Path, StringComparison.CurrentCultureIgnoreCase) == 0 ||
                    string.Compare(mapping.Location, request.AppRelativeCurrentExecutionFilePath, StringComparison.CurrentCultureIgnoreCase) == 0 ||
                    string.Compare(mapping.Location, request.RawUrl, StringComparison.CurrentCultureIgnoreCase) == 0)
                {
                    return mapping.Directaccessible;
                }
            }
            return isDirectaccessible;
        }

        /// <summary>
        /// Gets the name of the bookmark 
        /// </summary>
        /// <param name="request">Current <see cref="HttpRequest"/></param>
        /// <returns>name of the bookmark as defined in the web.config</returns>
        private string GetBookmarkName(HttpRequest request)
        {
            AspNavigationConfigSection settings = ConfigurationManager.GetSection(AspNavigationConfigSection.configSectionName) as AspNavigationConfigSection;
            foreach (PageMappingElement mapping in settings.PageMappings)
            {
                if (string.Compare(mapping.Location, request.Path, StringComparison.CurrentCultureIgnoreCase) == 0 ||
                    string.Compare(mapping.Location, request.AppRelativeCurrentExecutionFilePath, StringComparison.CurrentCultureIgnoreCase) == 0 ||
                    string.Compare(mapping.Location, request.RawUrl, StringComparison.CurrentCultureIgnoreCase) == 0)
                {
                    return mapping.Bookmark;
                }
            }
            return string.Empty;
        }

        string GetPageUrl(string bookmark)
        {
            string url = null;
            AspNavigationConfigSection settings = ConfigurationManager.GetSection(AspNavigationConfigSection.configSectionName) as AspNavigationConfigSection;
            foreach (PageMappingElement mapping in settings.PageMappings)
            {
                if (string.Compare(mapping.Bookmark, bookmark, StringComparison.CurrentCultureIgnoreCase) == 0)
                {
                    url = mapping.Location;
                    break;
                }
            }

            return url;
        }

        string GetErrorPageUrl(Exception e)
        {
            string url = null;
            AspNavigationConfigSection settings = ConfigurationManager.GetSection(AspNavigationConfigSection.configSectionName) as AspNavigationConfigSection;
            foreach (ExceptionMappingElement mapping in settings.ExceptionMappings)
            {
                Type eType = Type.GetType(mapping.Type);
                //TODO:If type is null - should
                //throw a meaningful exception- WorkflowNotFoundException?
                if (eType.IsAssignableFrom(e.GetType()))
                {
                    url = mapping.Location;
                    break;
                }
            }

            return url;
        }

        private void DisplayPage(HttpContext httpContext, Guid workflowId, InteractionContext interactionContext)
        {
            string url = null;
            if (interactionContext.Error == null || interactionContext.Error is UserInputValidationException)
            {
                url = GetPageUrl(interactionContext.Bookmark.ActivityName);
                if (string.IsNullOrEmpty(url))
                    throw new InvalidOperationException(SR.GetString(SR.Error_CantFindMatchingUrl, interactionContext.Bookmark.ActivityName));
                httpContext.Session[SessionVariableLookupKeys.InteractionContext] = interactionContext;
                httpContext.Response.Redirect((url + @"?FlowId=" + workflowId.ToString() + @"&InteractionId=" + interactionContext.Bookmark.InteractionId.ToString()), false);
            }
            else if (interactionContext.Error != null)
            {
                DisplayException(httpContext, workflowId, interactionContext.Error);
            }
        }

        private void DisplayException(HttpContext httpContext, Guid workflowId, Exception exception)
        {
            string url = GetErrorPageUrl(exception);
            if (!string.IsNullOrEmpty(url))
            {
                //don't overwrite last exception
                if (httpContext.Session[SessionVariableLookupKeys.Exception] == null)
                {
                    httpContext.Session[SessionVariableLookupKeys.Exception] = exception;

                }
                httpContext.ClearError();
                ClearSession();
                httpContext.Response.Redirect(url + @"?FlowId=" + workflowId.ToString(), false);
            }
            else
            {
                httpContext.Session.Remove(SessionVariableLookupKeys.Exception);
                throw exception;
            }
        }

        private void OnWorkflowCompleted(object sender, WorkflowCompletedEventArgs e)
        {
            ClearSession();
        }
        private void ClearSession()
        {
            HttpContext httpContext = GetHttpContext();
            if (httpContext != null)
                httpContext.Session.Remove(SessionVariableLookupKeys.InstanceId);
        }
        private void OnWorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
        {
            HttpContext httpContext = GetHttpContext();
            if (e.Exception != null)
            {
                DisplayException(httpContext, e.WorkflowInstance.InstanceId, e.Exception);
            }
            ClearSession();
        }

        internal static HttpContext GetHttpContext()
        {
            HttpContext currentContext = HttpContext.Current;
            if (currentContext == null)
            {
                throw new InvalidOperationException(SR.GetString(SR.Error_CantGetHttpContext));
            }

            return currentContext;
        }
    }

    class ApplicationVariableLookupKeys
    {
        internal const string NavigationManager = "__NavigationManager__";
    }

    class SessionVariableLookupKeys
    {
        internal const string InstanceId = "__WorkflowInstanceId__";
        internal const string UserInput = "__UserInput__";
        internal const string InteractionContext = "__InteractionContext__";
        internal const string Exception = "__Exception__";
    }
}
